Day 14 已經完成任務編輯。
目前 CRUD 已經有:
Create
→ 新增任務
Read
→ 顯示任務
Update
→ 編輯任務
今天要補上最後一塊:
Delete
→ 刪除任務
Day 15 的目標很單純:
讓首頁上的任務可以被刪除,而且真的同步刪掉 MySQL 裡的資料。
今天只做:
首頁按「刪除」
↓
跳出確認視窗
↓
送出 POST
↓
Flask 找到指定 Task
↓
從 MySQL 刪除
↓
回首頁
今天先不做:
篩選
快速切換狀態
分類 CRUD
services/
routes/
Claude API
我先把需求交給 Claude Code:
目前狀態:
1. /tasks/new 可以新增任務
2. 首頁可以從 MySQL 讀取任務
3. /tasks/<task_id>/edit 可以編輯任務
4. Task CRUD 已經完成 Create / Read / Update
今天只做「刪除任務」。
需求:
1. 首頁每張任務卡片增加「刪除」按鈕
2. 使用 POST 送出刪除請求
3. Flask 根據 task_id 找到任務
4. 找不到任務時回 404
5. 找到後用 db.session.delete(task)
6. db.session.commit()
7. 刪除成功後 redirect 回首頁
8. 不使用 GET 直接刪除資料
9. 加入瀏覽器原生 confirm(),避免誤刪
今天不要:
- 篩選
- 快速切換狀態
- 分類 CRUD
- services/
- routes/
- Claude API
請先規劃會修改哪些檔案,
不要直接修改程式。
最後只修改:
app.py
templates/index.html
static/css/style.css
不需要新增新的頁面,也不用修改資料庫結構。
這次新增:
@app.route("/tasks/<int:task_id>/delete", methods=["POST"])
def delete_task(task_id):
task = Task.query.get_or_404(task_id)
db.session.delete(task)
db.session.commit()
return redirect(url_for("index"))
這段就是 Day 15 最核心的程式。
例如首頁有:
id = 3
title = 測試1
按下這筆任務的刪除按鈕後,會送到:
/tasks/3/delete
所以 Flask 收到:
task_id = 3
接著:
task = Task.query.get_or_404(task_id)
去 MySQL 找:
id = 3
那筆資料。
如果找不到,就直接回:
404 Not Found
Day 12 新增資料時,用的是:
db.session.add(task)
Day 15 刪除則是:
db.session.delete(task)
可以理解成:
add()
→ 準備新增
delete()
→ 準備刪除
但真正把變更寫進 MySQL,還是要:
db.session.commit()
所以:
找到 Task
↓
delete(task)
↓
commit()
↓
MySQL DELETE
這裡我原本也有疑問。
既然做的是「刪除」,為什麼 Route 是:
@app.route("/tasks/<int:task_id>/delete", methods=["POST"])
不是:
DELETE
原因是目前這個專案使用一般的 HTML <form>。
原生 HTML form 主要支援:
GET
POST
不能直接這樣寫:
<form method="DELETE">
所以目前 Flask + Jinja2 的做法,直接使用:
POST /tasks/3/delete
會比較簡單。
如果未來改成 REST API,可能會使用:
DELETE /tasks/3
例如搭配 JavaScript:
fetch("/tasks/3", {
method: "DELETE"
})
但目前專案沒有使用 AJAX,所以 Day 15 繼續用 POST。
可以簡單記成:
目前 HTML 表單網站
→ POST /tasks/3/delete
REST API 常見寫法
→ DELETE /tasks/3
如果寫成:
GET /tasks/3/delete
只要有人打開網址,就會把資料刪掉。
但 GET 比較適合:
查看頁面
讀取資料
刪除會改變資料,所以不應該使用 GET。
這也是為什麼這次 Route 特別限定:
methods=["POST"]
首頁原本 Day 14 已經有:
<a
class="edit-link"
href="{{ url_for('edit_task', task_id=task.id) }}">
編輯
</a>
Day 15 在旁邊加入:
<form
class="delete-form"
method="POST"
action="{{ url_for('delete_task', task_id=task.id) }}"
onsubmit="return confirm('確定要刪除這個任務嗎?');">
<button class="delete-button" type="submit">
刪除
</button>
</form>
所以每張任務卡片現在都有:
編輯 刪除

如果按一下就直接刪掉,太容易誤操作。
所以表單加上:
onsubmit="return confirm('確定要刪除這個任務嗎?');"
按下「刪除」後,瀏覽器會先跳出確認視窗。
也就是:
刪除
↓
confirm()
↓
取消 → 不刪
確定 → 才送 POST
這次新增:
.delete-form {
display: inline-block;
margin: 6px 0 0 12px;
}
因為 <form> 原本比較像區塊元素,這樣可以讓它跟「編輯」比較自然地排在一起。
刪除按鈕:
.delete-button {
padding: 0;
border: 0;
background: none;
color: var(--todo-text);
font-size: 0.85rem;
font-weight: 600;
}
再加:
.delete-button:hover {
text-decoration: underline;
}
這樣就不會像一般大型按鈕,而是比較像文字操作。
目前刪除使用的是:
var(--todo-text)
所以會呈現偏橘色。
這不影響功能,之後如果想整理 CSS,可以再另外定義:
--danger
專門給刪除這類危險操作使用。
我先在首頁確認每張任務都有:
編輯
刪除
接著按下「刪除」。
瀏覽器會先跳出:
首頁重新整理後,該筆任務已經不在列表中。
接著回 MySQL Workbench:
SELECT * FROM tasks;
確認剛才刪掉的 Task 已經從 tasks 資料表消失。
這代表流程不是只有前端把卡片隱藏,而是真的:
首頁
↓
POST
↓
Flask
↓
SQLAlchemy
↓
MySQL DELETE
資料庫裡也確實刪除成功。
Route 最後:
return redirect(url_for("index"))
刪除完成後回到:
/
而 Day 13 的首頁本來就會重新查:
Task.query.order_by(
Task.created_at.desc(),
Task.id.desc(),
).all()
所以被刪掉的任務自然就不會再出現。
做到 Day 15,任務已經有完整 CRUD:
Create
→ 新增任務
Read
→ 查看任務
Update
→ 編輯任務
Delete
→ 刪除任務
這時候這個 Web App 已經真的可以管理 MySQL 裡的 Task。
不是只有畫面,也不是假資料。
目前可以:
建立任務
查看任務
修改任務
刪除任務
而且每個動作都會同步到資料庫。
Day 15 完成後,Task 的基本 CRUD 已經補完整。
接下來不再只是「資料能不能新增修改」,而是開始處理任務本身的狀態管理。
目前資料庫其實已經有:
todo
in_progress
done
但首頁上方的:
全部
待處理
進行中
已完成
目前還只是畫面,沒有真的篩選資料。
下一步就開始處理這部分。